-- Copyright (c) 2024 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at:
--         http://license.coscl.org.cn/MulanPSL2
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.

local skynet = require 'skynet'
local skynet_queue = require 'skynet.queue'
local libmgmt_protocol = require 'libmgmt_protocol'
local mdb = require 'mc.mdb'
local log = require 'mc.logging'
local utils = require 'mc.utils'
local class = require 'mc.class'
local file_sec = require 'utils.file'
local vos = require 'utils.vos'
local common_def = require 'common_def'
local mctp_lib = require 'mctp_lib'
local client = require 'storage.client'
local storage_bus = require 'storage_bus'
local dump_nvme_log = require 'nvme.nvme_mi_protocol.nvme_mi_dump_log'
local nvme_admin_command = require 'nvme.nvme_mi_protocol.nvme_mi_admin_command'
local nvme_mi_command = require 'nvme.nvme_mi_protocol.nvme_mi_command'

local MODULE_NAME_STORAGE_MGNT = 'Storage'
local DRIVEDISK_LOG_PATH = "/data/var/log/storage/drivelog"
local DRIVEDISK_LOG_PATH_F = "/data/var/log/storage/drivelog/Disk%s"
local DISK_PATH = "/data/var/log/storage/drivelog/Disk%s/nvme.log"
local SMART_LOG_PATH = "/data/var/log/storage/drivelog/Disk%s/nvme_smart.log"
local MAX_RETRY = 120
local NVME_SMBUS_PHY_ADDR = 29

local c_nvme_mctp = class()

function c_nvme_mctp:ctor(nvme)
    self.nvme = nvme
    self.nvme_mi_obj = nil
    self.smart_log = {}
    self.feature_identifiers = {}
    self.fw_log = {}
    self.supported_log_pages = {}
    self.error_log = {}
    self.controller_health_status = {}
    self.mctp_trans_unit_size = {}
    self.nvm_subsystem_info = {}
    self.subsys_health_status = {}
    self.ctrl_id = 0
    self.queue = skynet_queue()
end

function c_nvme_mctp:create_nvmemi_endpoint()
    if self.nvme.protocol ~= common_def.NVME_VPD_PROTOCOL_NVME_MI then
        return
    end

    local devbus, devdevice, devfun = self:get_pcie_device_info()
    if not devbus or not devdevice or not devfun then
        log:error('NVMe:%s get bdf failed from pcie.', self.nvme.Slot)
        return
    end

    if devbus == 0 and devdevice == 0 and devfun == 0 then
        log:error('NVMe:%s get invalid bdf from pcie.', self.nvme.Slot)
        return
    end
    local phy_addr = mctp_lib.bdf_to_phy_addr(devbus, devdevice, devfun)
    log:notice('get nvme phy_addr: %s', phy_addr)
    self:create_endpoint_and_transport_obj(phy_addr)
end

function c_nvme_mctp:update_mctp_properties()
    skynet.fork_once(function()
        if not self.nvme_mi_obj then
            return
        end
        nvme_admin_command.update_admin_command_info(self)
        nvme_mi_command.update_nvme_mi_command_info(self)
    end)
end

function c_nvme_mctp:update_smart_log()
    nvme_admin_command.update_smart_log(self)
end

function c_nvme_mctp:collect_nvme_smart_log()
    if not vos.get_file_accessible(DRIVEDISK_LOG_PATH) then
        utils.mkdir_with_parents(DRIVEDISK_LOG_PATH, utils.S_IRWXU | utils.S_IRGRP | utils.S_IXGRP)
    end
    if file_sec.check_realpath_before_open_s(DRIVEDISK_LOG_PATH) == common_def.RET_OK then
        utils.mkdir_with_parents(string.format(DRIVEDISK_LOG_PATH_F, self.nvme.Slot),
            utils.S_IRWXU | utils.S_IRGRP | utils.S_IXGRP)
    end

    local fp_w, err = file_sec.open_s(string.format(SMART_LOG_PATH, self.nvme.Slot), 'w+')
    if not fp_w then
        log:error('open file failed, err: %s', err)
        return
    end
    log:notice('start dump Disk%s nvme smart log', self.nvme.Slot)
    dump_nvme_log.dump_smart_log(self, fp_w)
    log:notice('finish dump Disk%s nvme smart log', self.nvme.Slot)
    fp_w:close()
end

function c_nvme_mctp:collect_nvme_log()
    if not vos.get_file_accessible(DRIVEDISK_LOG_PATH) then
        utils.mkdir_with_parents(DRIVEDISK_LOG_PATH, utils.S_IRWXU | utils.S_IRGRP | utils.S_IXGRP)
    end
    if file_sec.check_realpath_before_open_s(DRIVEDISK_LOG_PATH) == common_def.RET_OK then
        utils.mkdir_with_parents(string.format(DRIVEDISK_LOG_PATH_F, self.nvme.Slot),
            utils.S_IRWXU | utils.S_IRGRP | utils.S_IXGRP)
    end

    local fp_w, err = file_sec.open_s(string.format(DISK_PATH, self.nvme.Slot), 'w+')
    if not fp_w then
        log:error('open file failed, err: %s', err)
        return
    end
    log:notice('start dump Disk%s nvme mi mctp log', self.nvme.Slot)
    dump_nvme_log.dump_log(self, fp_w)
    fp_w:close()
    log:notice('finish dump Disk%s nvme mi mctp log', self.nvme.Slot)
end

function c_nvme_mctp:get_pcie_device_info()
    local objs = client:GetPCIeDeviceObjects()
    if not next(objs) then
        log:error('get PCIeDevice obj failed')
        return
    end
    for _, dev_obj in pairs(objs) do
        -- pcie设备对象比nvme对象先加载一级，通过position匹配对象
        if string.find(self.nvme.position, dev_obj.extra_params.Position) then
            log:notice('DeviceName: %s, DevBus: %s, DevDevice: %s, DevFunction: %s, nvme position: %s',
                dev_obj.DeviceName, dev_obj.DevBus, dev_obj.DevDevice, dev_obj.DevFunction, self.nvme.position)
            return dev_obj.DevBus, dev_obj.DevDevice, dev_obj.DevFunction
        end
    end
end

function c_nvme_mctp:create_mctp_endpoint_for_smbus(position)
    local count = 0
    while count < MAX_RETRY do
        local bus = storage_bus.get_instance().bus
        local ok, endpoint, transport = pcall(mctp_lib.get_endpoint_and_transport, bus, MODULE_NAME_STORAGE_MGNT,
            NVME_SMBUS_PHY_ADDR, mctp_lib.MCTP_MESSAGE_TYPE_NVME, position)
        if ok and endpoint and transport then
            ok, nvme_mi_config = pcall(require, 'nvme.nvme_mi_protocol.nvme_mi')
            if not ok or not nvme_mi_config then
                log:error("require nvme_mi protocol failed: %s", nvme_mi_config)
            else
                self.nvme_mi_obj = libmgmt_protocol.device_spec_parser(nvme_mi_config.mctp(endpoint))
                log:notice('build mctp over smbus success, count: %s', count)
                return
            end
        end
        count = count + 1
        log:notice('build mctp over smbus failed, count: %s, ret: %s', count, endpoint)
        utils.msleep(1000)
    end
end

function c_nvme_mctp:create_endpoint_and_transport_obj(phy_addr)
    local count = 0
    local MCTP_ROUTING_PATH = '/bmc/kepler/Systems/1/Mctp/Routing'
    local MCTP_ROUTING_INTERFACE = 'bmc.kepler.Systems.Mctp.PCIeRoutingEntry'
    local ok, endpoint, transport, hardware_config, obj_list, match_flag
    local ret, err
    while count < MAX_RETRY do
        ret, err = pcall(function ()
            ok, obj_list =
                pcall(mdb.get_sub_objects, storage_bus.get_instance().bus, MCTP_ROUTING_PATH, MCTP_ROUTING_INTERFACE)
            if not ok then
                log:info('get mctp routing list failed, Disk%s phy_addr:%s', self.nvme.Slot, phy_addr)
                error(obj_list)
            end
            for _, obj in pairs(obj_list) do
                local addr = string.byte(obj.PhysAddress)
                if addr == phy_addr then
                    match_flag = true
                    break
                end
            end
            if not match_flag then
                error('No match phy_addr')
            end
            ok, endpoint, transport = pcall(mctp_lib.get_endpoint_and_transport, storage_bus.get_instance().bus,
                MODULE_NAME_STORAGE_MGNT, phy_addr, mctp_lib.MCTP_MESSAGE_TYPE_NVME)
            if not ok or not endpoint or not transport then
                log:error('unable to create mctp transport and endpoint. msg: %s', endpoint)
                error(endpoint)
            end
            log:notice('creat disk%s endpoint successfully', self.nvme.Slot)
        end)
        if not ret then
            log:info('create Disk%s endpoint failed, err:%s', self.nvme.Slot, err)
            goto continue
        end
        ok, hardware_config = pcall(require, 'nvme.nvme_mi_protocol.nvme_mi')
        if not ok or not hardware_config then
            log:error('require nvme_mi failed: %s', hardware_config)
        else
            self.nvme_mi_obj = libmgmt_protocol.device_spec_parser(hardware_config.mctp(endpoint))
            return
        end
        ::continue::
        count = count + 1
        utils.msleep(1000)
    end
    log:raise('unable to create mctp transport and endpoint with bdf %s', phy_addr)
end

return c_nvme_mctp